//
//  DPUnitTestMain.m
//  DPUnitTest
//
//  Created by Ofri Wolfus on 07/10/06.
//  Copyright 2006 Ofri Wolfus. All rights reserved.
//

#include <objc/objc-class.h>
#include <objc/objc-runtime.h>
#include <unistd.h>
#import "DPUnitTest.h"

#ifndef _C_CONST
#define _C_CONST		'r'
#endif

#ifndef _C_IN
#define _C_IN			'n'
#endif

#ifndef _C_INOUT
#define _C_INOUT		'N'
#endif

#ifndef _C_OUT
#define _C_OUT			'o'
#endif

#ifndef _C_BYCOPY
#define _C_BYCOPY		'O'
#endif

#ifndef _C_BYREF
#define _C_BYREF		'R'
#endif

#ifndef _C_ONEWAY
#define _C_ONEWAY		'V'
#endif

#ifndef _C_GCINVISIBLE
#define _C_GCINVISIBLE	'!'
#endif


@interface DPUnitTest (private)
- (void)_DPThrow:(NSException *)e;
- (NSString *)failReason;
@end


BOOL DPBlockUnitTestsExceptions = NO;
typedef BOOL (* hom_BOOL_msgSend)(id self, SEL op, ...);

static inline char * dput_getReturnType(Method m) __attribute__((always_inline));
static inline char * dput_getReturnType(Method m) {
	char	*type = m->method_types;
	
	//Skip type qualifiers
	while (*type == _C_CONST
		   || *type == _C_IN
		   || *type == _C_INOUT
		   || *type == _C_OUT
		   || *type == _C_BYCOPY
		   || *type == _C_BYREF
		   || *type == _C_ONEWAY
		   || *type == _C_GCINVISIBLE)
    {
		type += 1;
    }
	
	return type;
}

static inline BOOL dput_isSubclassOfClass(Class obj, Class cls)	__attribute__((always_inline));
static inline BOOL dput_isSubclassOfClass(Class cls, Class super) {
	while ((cls = cls->super_class)) {
		if (cls == super)
			return YES;
	}
	
	return NO;
}

static inline BOOL dput_testIsDisabledDisabledTests(Class testCase) {
	NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
	NSDictionary	*env = [[NSProcessInfo processInfo] environment];
	NSEnumerator	*e = [env keyEnumerator];
	NSString		*str, *name = NSStringFromClass(testCase);
	BOOL			r = NO;
	
	while ((str = [e nextObject]) && !r)
		if ([name hasPrefix:str])
			r = [[env valueForKey:str] isEqualToString:@"NO"];
	
	[p release];
	return r;
}


static inline BOOL dput_leaksModeEnabled() {
	NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
	NSDictionary *env = [[NSProcessInfo processInfo] environment];
	NSString *str = [env valueForKey:@"MallocHeapLogging"];
	BOOL r = NO;
	if (str)
		r = [str isEqualToString:@"YES"];
	else {
		str = [env valueForKey:@"MallocStackLogging"];
		r = str != nil && [str isEqualToString:@"1"];
	}
	
	[p release];
	return r;
}


#if defined (DPUT_NO_INIT)
int DPUTInitialized = 0;
#else
int DPUTInitialized = 1;
#endif

#if defined (DEBUG_BUILD)
static FILE *output_file = stderr;
#else
static FILE *output_file = stdout;
#endif

void DPUTRunTests() {
	if (!DPUTInitialized) {
		int count = objc_getClassList(NULL, 0);			// The number of classes registered with the ObjC runtime
		Class *classes = calloc(count, sizeof(Class));	// A block of memory for pointers to all classes
		Class testCls = objc_getClass("DPUnitTest");	// The class for simple unit tests
		Class speedCls = objc_getClass("DPSpeedTest");	// The class of speed tests
		int i;
		
		// Get an array of all registered classes
		objc_getClassList(classes, count);
		
		// Loop through all classes and find subclasses of DPUnitTest to work with
		for (i = 0; i < count; i++) {
			Class cls = classes[i];
			
			// Is our class a subclass of DPUnitTest?
			if (cls != speedCls && dput_isSubclassOfClass(cls, testCls)) {
				// Break if our test is disabled
				if (dput_testIsDisabledDisabledTests(cls))
					continue;
				
				NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];	// Everything in Cocoa needs an autorelease pool
				void		*iterator = 0;		// An iterator for class_nextMethodList()
				struct		objc_method_list *mlist;	// A list of methods that our class implements
				id			instance = [[cls alloc] init];	// An instance of our class on which we'll invoke our test methods
				NSString	*clsName = [[cls className] retain];	// The name of our class
				BOOL		throws = [instance throwsForFaildTests];	// Are we supposed to throw an exception when a test fails?
				BOOL		logs = [instance logsSuccessfulTests];		// Should we log successful tests?
				BOOL		isSpeedTest = dput_isSubclassOfClass(cls, speedCls);
				unsigned	iterNum = isSpeedTest ? [instance numberOfIterations] : 1U;	// The number of times we should run every test
				BOOL		infoDisplayed = NO;		// Inidicates whether the beginning message was displayed
				BOOL		verifyAllTests = isSpeedTest ? [instance verifiesAllTests] : NO;
				Method		defaultValidateMeth = class_getInstanceMethod(cls, @selector(verifyResult:ofTest:));
				
				// Loop through all methods lists of this class
				while ((mlist = class_nextMethodList(cls, &iterator))) {
					int j;
					
					// Loop through the methods in this method list and find the ones which are tests
					for (j = 0; j < mlist->method_count; j++) {
						Method m = &(mlist->method_list[j]);
						const char *methName = sel_getName(m->method_name);
						
						// We invoke only methods that their name begins with 'test'
						if (!strncmp(methName, "test", 4)) {
							unsigned		l;	/* Loop counter */
							CFAbsoluteTime	time = 0;	/* Total time for this test */
							CFAbsoluteTime	t;	/* Time of a single call */
							BOOL			b = YES;	/* A flag indicating the success of the last test */
							Method			verifyMeth = NULL;
							
							if (isSpeedTest) {
								size_t			selNameSize = sizeof(char) * (strlen(methName) + 6 + 2); /* == strlen("verify") + ':' + NULL */
								char			*verifyMethName = malloc(selNameSize);
								SEL				verifySel;
								
								strlcat(verifyMethName, "verifyTest", selNameSize);
								strlcat(verifyMethName, methName + 4 /* skip the 'test' at the beginning */, selNameSize);
								strlcat(verifyMethName, ":", selNameSize);
								verifySel = sel_registerName(verifyMethName);
								verifyMeth = class_getInstanceMethod(cls, verifySel) ?: defaultValidateMeth;
								
								// Clean up
								free(verifyMethName);
							}
							
							if (!infoDisplayed) {
								//NSLog(@"=== Starting tests in %@ ===", clsName);
								fprintf(output_file, "=== Starting tests in %s ===\n", [clsName UTF8String]);
								infoDisplayed = YES;
							}
							
							// Invoke our method as many times as we're told to.
							// This is customizable per test class through overriding -[DPSpeedTest numberOfIterations]
							for (l = 0U; l < iterNum; l++) {
								id result;
								
								// Clean up our autorelease pool
								[pool release]; pool = [[NSAutoreleasePool alloc] init];
								
								if (*dput_getReturnType(m) == *@encode(BOOL)) {
									// Get the current time
									t = CFAbsoluteTimeGetCurrent();
									// Invoke our method (this is not a message call, but a function call).
									b = ((hom_BOOL_msgSend)m->method_imp)(instance, m->method_name);
									// The time took for this test is the current time - the time before invoking the test
									t = CFAbsoluteTimeGetCurrent() - t;
									// Add it to the total
									time += t;
								} else {
									// Get the current time
									t = CFAbsoluteTimeGetCurrent();
									// Invoke our method (this is not a message call, but a function call).
									result = m->method_imp(instance, m->method_name);
									// The time took for this test is the current time - the time before invoking the test
									t = CFAbsoluteTimeGetCurrent() - t;
									// Add it to the total
									time += t;
								}
								
								// Let our instance a chance to verify the result of this test
								if (verifyMeth && (l == 0 || verifyAllTests))
									b = ((hom_BOOL_msgSend)(verifyMeth->method_imp))(instance, verifyMeth->method_name, result, m->method_name);
								
								// Test methods may either return BOOL or void.
								// If the return type is BOOL, it indicates whether the test succeeded or not.
								if ((*dput_getReturnType(m) == *@encode(BOOL) || verifyMeth) && !b) {
									// Our test may have given us a detailed explanation why it failed.
									// If not, -failReason will return nil.
									NSString *failReason = [instance failReason];
									// Build our fail message
									NSMutableString *msg = [NSMutableString stringWithFormat:@"*** %@ failed.", NSStringFromSelector(m->method_name)];
									
									// Append the fail reason to it
									if (failReason)
										[msg appendFormat:@" %@", failReason];
									
									// Clean up the fail reason
									[instance setFailReason:nil];
									
									// Throw an exception if needed,
									if (throws)
										// We let our instance throw the exception so that when running under the debugger,
										// the user will be able to examine instance variables if needed.
										[instance _DPThrow:[NSException exceptionWithName:NSInternalInconsistencyException
																				   reason:msg
																				 userInfo:nil]];
									// Or just log it
									else
										//NSLog(@"%@", msg);
										fprintf(output_file, "%s\n\n", [msg UTF8String]);
									
									// Since our test failed, we won't continue this loop
									break;
									
								// Make sure b == YES if our method returns something other than BOOL.
								// Although this is not pretty, it's the shortest way I can think of for making
								// sure logging will occure in case of non-BOOL, non-void return types.
								} else {
									b = YES;
								}
							}
							
							// Log if the test succeeded (b == YES), and logging is enabled.
							// This includes the avarage time of this test.
							if (logs && b)
								//NSLog(@"-[%@ %@] passed successfully.\nTest took %f seconds", clsName, NSStringFromSelector(m->method_name), time/iterNum);
								fprintf(output_file, "* %s passed successfully. Test took %f seconds.\n\n", sel_getName(m->method_name), time/iterNum);
						}
					}
				}
				
				// Let our instance a chance to display any info vefore the test is complete
				[instance release];
				
				// Log if needed
				if (infoDisplayed)
					//NSLog(@"=== Tests in %@ finished successfully. ===\n\n", clsName);
					fprintf(output_file, "=== Tests in %s finished successfully. ===\n\n\n", [clsName UTF8String]);
				
				// Clean our autorelease pool
				[pool release];
			}
		}
		
		// Clean up some more
		free(classes);
		DPUTInitialized = 1;
	}
}

#if defined (DPUT_NO_INIT)
__attribute__((destructor)) void _DPUTInit() {
	DPUTRunTests();
}
#else
int main (int argc, const char * argv[]) {
	DPUTInitialized = 0;
	DPUTRunTests();
	// Loop if we're running under MallocDebug
	if (dput_leaksModeEnabled())
		while (1);
	return 0;
}
#endif
